#ifndef __WIN_SOCKET_SERVER_H__
  #define __WIN_SOCKET_SERVER_H__

#include <string>
#include <WinSock.h>

#include "exceptions.h"
#include "converter.h"

#define WIN_SOCKET_SERVER_BUFFER_SIZE (4 *1024)

using namespace std;

//! Server for network communications

/*!
This class implements a simple server. This class can be freely used as a web server.

Implementation date: 2008

Usage:

\code
unsigned int iterations = UINT_MAX;

win_socket_server wss;
wss.start_server(80, iterations, respond_method); // on port 80, unlimited iteration(s)
\endcode
*/

class win_socket_server {
private:
  string local_host_ip_address;
  string last_client_ip;
  sockaddr_in local;
  SOCKET s;
  SOCKET my_server_socket;

  //! receives data from the client
  void receive_data(char *temp, string &str_msg) {
    int i = 0;

    do {
      memset(temp, 0, WIN_SOCKET_SERVER_BUFFER_SIZE + sizeof(char));
      i = recv(my_server_socket, temp, WIN_SOCKET_SERVER_BUFFER_SIZE * sizeof(char), 0);
      if (i > 0)
        str_msg.append(temp, i);

    } while (i == WIN_SOCKET_SERVER_BUFFER_SIZE * sizeof(char));
  }

  //! sends the data to the client. 'str_output' will be deleted before this function returns
  void send_data(char *&str_output, int &output_len) {
    if (output_len != 0) {
      int total_bytes_sent = 0;
      int i = 0;

      do {
        i = send(my_server_socket, str_output + (i * sizeof(char)), (output_len - total_bytes_sent) * sizeof(char), 0);
        if (i < 0) {
          delete[] str_output;
          str_output = 0;
          output_len = 0;
          throw socket_exception("Socket send error");
        } // if

        total_bytes_sent += i;
      } while (total_bytes_sent < output_len);
    } // if

    delete[] str_output;
    str_output = 0;
    output_len = 0;
  }

public:
  //! sets the ip address of the server (useful if more than one network card is installed)
  void set_local_host_ip_address(const string &ip_address) {
    this->local_host_ip_address = ip_address;
  }

  //! returns the ip address of the last client connected to the server
  const string &get_ip_address_of_last_client() const {
    return last_client_ip;
  }

  //! starts the server and waits for requests
  void start_server(unsigned short port, unsigned int &iterations, void (*call_back_method)(const string &ip_address, const string &input, char *&output, int &output_len)) {
    local.sin_family = AF_INET; // address family
    local.sin_port = htons(port); // port to use

    local.sin_addr.s_addr = INADDR_ANY; // wild card IP address

    if (local_host_ip_address.empty() == false) {
      local.sin_addr.s_addr = inet_addr(local_host_ip_address.c_str());
      if (local.sin_addr.s_addr == INADDR_NONE)
        local.sin_addr.s_addr = INADDR_ANY; // in case of error we set it back to wild card IP address
    } // if

    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, "1", strlen("1")) < 0)
      throw socket_exception("Setting socket option(s) failed!");

    struct timeval tv;
    tv.tv_sec = 10; // timeout in seconds
    tv.tv_usec = 0;

    if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*) &tv, sizeof(tv)) < 0)
      throw socket_exception("Setting socket option(s) failed!");

    if (::bind(s, (sockaddr*) &local, sizeof(local)) != 0) {
      string str_cnv;
      converter::to_string(port, str_cnv);
      throw socket_exception("Socket bind error on port: " + str_cnv);
    } // if

    if (listen(s, 10) != 0)
      throw socket_exception("Socket listen error");

    sockaddr_in from;
    int fromlen = sizeof(from);

    char temp[WIN_SOCKET_SERVER_BUFFER_SIZE + 1] = { "\0" };
    string str_msg; // message from the client
    char *str_output = 0; // data to send back to the client
    int output_len = 0;

    str_msg.reserve(WIN_SOCKET_SERVER_BUFFER_SIZE);
    while (iterations != 0) {
      my_server_socket = accept(s, (struct sockaddr*) &from, &fromlen);

      const char * const address = inet_ntoa(from.sin_addr);
      if (address != 0)
        last_client_ip = address;
      else
        last_client_ip.clear();

      receive_data(temp, str_msg);
      call_back_method(last_client_ip, str_msg, str_output, output_len);
      send_data(str_output, output_len);

      if (closesocket(my_server_socket) != 0) // closes the client socket
        throw socket_exception("Socket close error");

      str_msg.resize(0);

      if (iterations != UINT_MAX)
        --iterations;
    } // while
  }

  //! default constructor (may throw an exception)
  win_socket_server() {
    WSADATA wsaData;
    const int wsaret = WSAStartup(0x101, &wsaData); // WSAStartup returns zero on success.
    if (wsaret != 0)
      throw socket_exception("Socket init error");

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == INVALID_SOCKET) {
      WSACleanup();
      throw socket_exception("Socket init error");
    } // if
  }

  //! destructor
  virtual ~win_socket_server() {
    if (closesocket(s) != 0) {
      WSACleanup();
      throw socket_exception("Socket close error");
    } // if

    WSACleanup();
  }
};

#endif
